pub mod errors {
    pub struct IoErrorInfo {
        pub code: u32,
    }

    #[repr(C)]
    pub enum DatabaseError {
        /// Denotes an error when the user attempted a write operation.
        IsReadOnly,
        /// Denotes an error when the user should read the error code for what it was.
        IoError(IoErrorInfo),
        /// Denotes an error when the user should run a repair tool to recover it.
        FileCorrupted(u32),
    }

    impl From<DatabaseError> for libc::c_int {
        fn from(err: DatabaseError) -> libc::c_int {
            match err {
                DatabaseError::IsReadOnly => 1,
                DatabaseError::IoError(_) => 2,
                DatabaseError::FileCorrupted(_) => 3,
            }
        }
    }
}

pub mod c_api {
    use super::errors::DatabaseError;

    /// Converts a 'DatabaseError' value to *mut libc::c_char.
    ///
    /// # Safety
    ///
    /// This is an unsafe operation because it dereferences a passed pointer.
    #[no_mangle]
    pub unsafe extern "C" fn db_error_description(
        error: *const DatabaseError,
    ) -> *mut libc::c_char {
        let db_error: &DatabaseError = unsafe {
            // SAFETY: the pointer's lifetime is greater than the current stack frame.
            &*error
        };

        let error_str: String = match db_error {
            DatabaseError::IsReadOnly => "Cannot write to read-only database".to_string(),
            DatabaseError::IoError(error) => {
                format!("I/O Error, error code: {}", error.code)
            }
            DatabaseError::FileCorrupted(code) => {
                dbg!(code);
                format!("File corrupted, run repair, error code: {}", &code)
            }
        };

        let c_error = unsafe {
            // SAFETY: copying error_str to an allocated buffer with a NUL
            // character at the end.
            let malloc: *mut u8 = libc::malloc(error_str.len() + 1) as *mut _;
            if malloc.is_null() {
                return std::ptr::null_mut();
            }

            let source = error_str.as_bytes().as_ptr();
            std::ptr::copy_nonoverlapping(source, malloc, error_str.len());
            std::ptr::write(malloc.add(error_str.len()), 0);

            malloc as *mut libc::c_char
        };

        c_error
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::CStr;

    use super::{c_api, errors::DatabaseError, errors::IoErrorInfo};

    fn assert_db_error_description(error: DatabaseError, expected_result: &str) {
        let result = unsafe { c_api::db_error_description(&error) };
        let c_str = unsafe { CStr::from_ptr(result) };
        let result = c_str.to_str();

        assert!(result.is_ok());
        assert_eq!(expected_result, result.unwrap());
    }

    #[test]
    fn db_error_as_is_read_only() {
        assert_db_error_description(
            DatabaseError::IsReadOnly,
            "Cannot write to read-only database",
        );
    }

    #[test]
    fn db_error_as_io_error() {
        assert_db_error_description(
            DatabaseError::IoError(IoErrorInfo { code: 1 }),
            "I/O Error, error code: 1",
        );
    }

    #[test]
    fn db_error_as_file_corrupted() {
        assert_db_error_description(
            DatabaseError::FileCorrupted(1),
            "File corrupted, run repair, error code: 1",
        );
    }
}
